import flask
import functools
import json
import logging
import string
import random

from . import APP_URL
from . import activitypub

log = logging.getLogger(__name__)

"""
def action(func):
    " ""
    This function creates a decorator to be applied to the methods of class
    Actor. It represents an ActivityStream Action.
    The decorator will first execute the decorated function, and then schedule
    the delivery of the returned Activity.
    
    NOTE The function that is decorated with this decorator MUST return an
         Activity.
    
    :param func: The function to be decorated.
    " ""
    
    @functools.wraps(func)
    def decorator(self, *args, **kwargs):
        " ""
        Send an activity.
        By default, the Activity is sent to the Actor followers collection.
        To override this behavior it's possible to call the function like this:
            actor.follow(..., to=[], cc=[], bto=[], bcc=[])
        
        https://www.w3.org/TR/activitypub/#delivery
        " ""
        
        forgefed_db = database.start_database_session()
        
        # Create the activity from the Actor by executing the action (function)
        activity = func(self, *args, **kwargs)
        
        # Assign an ID to the Activity
        activity['id'] = '{}/federation/activity/{}'.format(APP_URL, activitypub.new_activity_id())
        
        # Add publishing datetime
        #     - use UTC
        #     - remove microseconds, use HH:MM:SS only
        #     - add timezone info. There is also .astimezone() but it seems to
        #       return the wrong value when used with .utcnow(). Bug?
        #     - convert to ISO 8601 format
        activity['published'] = activitypub.format_datetime(datetime.datetime.utcnow())
        
        # By default we send the activity to the as:followers collection
        activity['to'] = [ self.followers_uri ]
        
        # Create the list of recipients
        recipients = []
        
        # TODO Check if it's possible to simplify this loop
        for field in [ 'to', 'cc', 'bto', 'bcc' ]:
            if field in kwargs:
                activity[field] = kwargs[field]
            
            if field in activity:
                if isinstance(activity[field], str):
                    recipients.append(activity[field])
                else:
                    recipients.extend(activity[field])
        
        # Save a copy of the Activity in the database and add it to the Actor's
        # OUTBOX before sending it
        forgefed_db.add(database.Activity(
            uri = activity['id'],
            actor_uri = activity['actor'],
            document = json.dumps(activity)))
        
        forgefed_db.add(database.Collection(
            uri = self.outbox_uri,
            item = activity['id']))
        
        forgefed_db.commit()
        forgefed_db.remove()
        
        # Now we are ready to POST to the remote actors
        
        # Before sending, remove bto and bcc according to spec.
        # https://www.w3.org/TR/activitypub/#client-to-server-interactions
        activity.pop('bto', None)
        activity.pop('bcc', None)
        
        # Stop here if there are no recipients.
        # https://www.w3.org/TR/activitypub/#h-note-8
        if len(recipients) == 0:
            log.debug('No recipients. Activity will not be sent.')
            return
        
        # Make sure the local actor has a GPG key before POSTing anything. The
        # remote Actor will use the key for verifying the Activity.
        database.GpgKey.test_or_set(self.local_uri, self.publickey_uri)
        
        # Create a new Celery task for each recipient. Activities are POSTed
        # individually because if one request fails we don't want to resend
        # the same Activity to *all* the recipients.
        for recipient in recipients:
            log.debug('Scheduling new activity: id={} recipient={}'.format(
                activity['id'], recipient))
            
            tasks.activity.post.delay(
                activity      = activity,
                recipient_uri = recipient,
                depth         = settings.DELIVERY_DEPTH)
    
    return decorator
"""

class Actor:
    def send_activity(self, activity):
        """
        This function prepares an Activity and then schedules a new delivery task.
        
        NOTE: This function should probably never be called from another module.
              Instead of using .send_activity(), use the specific functions for
              actions such as .accept(), .follow(), etc.
        
        :param activity: The Activity to be send.
        """
        
        #forgefed_db = database.start_database_session()
        forgefed_db = flask.g.forgefed
        
        # Set the JSON-LD context
        activity['@context'] = activitypub.jsonld_context
        
        # Assign an ID to the Activity
        activity['id'] = '{}/federation/activity/{}'.format(APP_URL, activitypub.new_activity_id())
        
        # Add "actor" property to the Activity, assuming "self" if missing
        if 'actor' not in activity:
            activity['actor'] = self.local_uri
        
        # Add publishing datetime
        #     - use UTC
        #     - remove microseconds, use HH:MM:SS only
        #     - add timezone info. There is also .astimezone() but it seems to
        #       return the wrong value when used with .utcnow(). Bug?
        #     - convert to ISO 8601 format
        activity['published'] = activitypub.format_datetime(datetime.datetime.utcnow())
        
        # By default, the Activity is sent to the Actor followers collection.
        # https://www.w3.org/TR/activitypub/#delivery
        if 'to' not in activity:
            activity['to'] = [ self.followers_uri ]
        
        # Create the list of recipients that we need to deliver to
        recipients = []
        
        for field in [ 'to', 'cc', 'bto', 'bcc' ]:
            if field in activity:
                if isinstance(activity[field], str):
                    recipients.append(activity[field])
                else:
                    recipients.extend(activity[field])
        
        # Remove duplicates in the list of recipients, so that we will schedule
        # only one task for the same recipient Actor.
        # NOTE list(set()) might change the order of the recipients because set()
        #      does not preserve the order. However this is not a problem because
        #      for each recipient a separate task is scheduled.
        recipients = list(set(recipients))
        
        # Save a copy of the Activity in the database
        forgefed_db.add(database.Activity(
            uri = activity['id'],
            actor_uri = activity['actor'],
            document = json.dumps(activity)))
        
        # Add the Activity to the Actor's OUTBOX before sending it
        forgefed_db.add(database.Collection(
            uri = self.outbox_uri,
            item = activity['id']))
        
        forgefed_db.commit()
        forgefed_db.remove()
        
        # Now we are ready to POST to the remote actors
        
        # Before sending, remove bto and bcc according to spec.
        # https://www.w3.org/TR/activitypub/#client-to-server-interactions
        activity.pop('bto', None)
        activity.pop('bcc', None)
        
        # Stop here if there are no recipients.
        # https://www.w3.org/TR/activitypub/#h-note-8
        if len(recipients) == 0:
            log.debug('No recipients. Activity will not be sent.')
            return
        
        # Make sure the local actor has a GPG key before POSTing anything. The
        # remote Actor will use the key for verifying the Activity.
        database.GpgKey.test_or_set(self.local_uri, self.publickey_uri)
        
        # Create a new Celery task for each recipient. Activities are POSTed
        # individually because if one request fails we don't want to resend
        # the same Activity to *all* the recipients.
        for recipient in recipients:
            log.debug('Scheduling new activity: id={} recipient={}'.format(
                activity['id'], recipient))
            
            tasks.activity.post.delay(
                activity      = activity,
                recipient_uri = recipient,
                depth         = settings.DELIVERY_DEPTH)

    #@action
    def accept(self, object_uri, *args, **kwargs):
        """
        Accept Activity.
        
        :param object_uri: URI of the ActivityPub object that was accepted.
        """
        
        return {
            'type':     'Accept',
            'actor':    self.local_uri,
            'object':   object_uri,
            **kwargs }
    
    #@action
    def create(self, object_uri, *args, **kwargs):
        """
        Create Activity.
        
        :param object_uri: URI of the ActivityPub object that was created.
        """
        
        return {
            'type':     'Create',
            'actor':    self.local_uri,
            'object':   object_uri }
    
    def follow(self, object_uri, *args, **kwargs):
        """
        Follow Activity.
        
        :param object_uri: URI of the ActivityPub actor to follow.
        """
        
        forgefed = flask.g.forgefed
        remote_actor = activitypub.fetch(object_uri)
        
        if not remote_actor:
            raise Exception('Could not fetch remote actor.')
        
        # We cache a copy of the remote actor for quick lookups. This is used for
        # example when listing "following/followers" collections. If we only
        # stored the actors' URI we would need to GET the JSON data every time.
        forgefed.merge(database.Cache(
            uri = object_uri,
            document = json.dumps(remote_actor)))
        
        # Add the remote actor to the "following" collection
        forgefed.merge(database.Collection(
            uri = self.following_uri,
            item = object_uri))
        
        # Add a feed
        forgefed.add(database.Feed(
            actor_uri = self.local_uri,
            content = flask.render_template('federation/feed/follow.html', actor=self.jsonld, object=remote_actor)
        ))
        
        self.send_activity({
            'type':   'Follow',
            'object': object_uri,
            'to':     object_uri
        })
    
    #@action
    def offer(self, object, *args, **kwargs):
        """
        Offer Activity.
        
        :param object: Object to offer. Either a URI or a dictionary.
        """
        
        return {
            'type':     'Offer',
            'actor':    self.local_uri,
            'object':   object }
    
    #@action
    def resolve(self, object, *args, **kwargs):
        """
        Resolve Activity.
        
        :param object: Object that has been resolved.
        """
        
        return {
            'type':     'Resolve',
            'actor':    self.local_uri,
            'object':   object }
    
    #@action
    def update(self, object, *args, **kwargs):
        """
        Update Activity.
        
        :param object: The object that was updated.
        """
        
        return {
            'type':     'Update',
            'actor':    self.local_uri,
            'object':   object }

class Person(Actor):
    def handle_incoming_activity(activity):
        pass
